﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
using System.IO;

namespace Cqrs.Common
{
    public class StateMachine : Dictionary<string, State>
    {

        public StateMachine()
        {
        }

        public StateMachine(XElement xmlFile)
        {
            SetupStateMachineFromXml(xmlFile);
        }

        private void SetupStateMachineFromXml(XElement scxml)
        {
            string initialState = "";
            if (scxml.Attributes("initial").Count() > 0)
            {
                initialState = scxml.Attributes("initial").First().Value;
            }

            foreach (var stateNode in scxml.Elements())
            {
                if (stateNode.Name.LocalName != "State" && stateNode.Name.LocalName != "Final")
                    break;

                string stateName = stateNode.Attributes("id").First().Value;
                State state = new State(stateName);
                if (string.Equals(stateName, initialState, StringComparison.OrdinalIgnoreCase))
                {
                    InitialState = state.Name;
                }

                AddState(state);
            }

            foreach (var stateNode in scxml.Elements())
            {
                foreach (var transition in stateNode.Elements())
                {
                    AddTransitionToState(stateNode.Attributes("id").First().Value, transition);
                }
            }
        }

        private void AddTransitionToState(string stateName, XElement transition)
        {

            if (transition.Attributes("event").Count() == 0)
                throw new Exception("The scxml file contains a transition with no event");

            string eventName = transition.Attributes("event").FirstOrDefault().Value;
            string commandName = transition.Attributes("command").FirstOrDefault().Value;

            this[stateName]
                .AddTransition(commandName,
                                eventName,
                                this[transition.Attribute("target").Value].Name);
        }

        private void AddState(State state)
        {
            this.Add(state.Name, state);
        }

        public string InitialState { get; set; }
        public string FinalState { get; set; }

        public List<string> States
        {
            get
            {
                return this.Select(p => p.Value.Name).ToList();
            }
        }

        public List<CommandEvent> UniqueCommandEventList
        {
            get
            {
                List<CommandEvent> returnVal = new List<CommandEvent>();
                foreach (var state in this)
                {
                    foreach (var transition in state.Value.Transitions)
                    {
                        var exists = returnVal.Where(
                            p => p.Command == transition.Value.Command &&
                                p.Event == transition.Value.Event
                                ).Any();
                        if (!exists)
                        {
                            CommandEvent commandEvent = new CommandEvent()
                            {
                                Command = transition.Value.Command,
                                Event = transition.Value.Event
                            };

                            returnVal.Add(commandEvent);
                        }
                    }
                }

                return returnVal;
            }
        }

        public List<List<TransitionTourStep>> TransitionTourSequence()
        {

            List<List<TransitionTourStep>> transitionTour = new List<List<TransitionTourStep>>();

            List<TransitionTourStep> commandSequence = new List<TransitionTourStep>();
            transitionTour.Add(commandSequence);

            // Get the input/output from the start State.
            State fromState = this[InitialState];
            State toState = this[fromState.Transitions.FirstOrDefault().Value.ResultingState];
            transitionTour = AddToTransitionTour(transitionTour, fromState, toState, commandSequence);

            // Remove any distinct Tours (bit of a hack, as sometimes it happens).
            transitionTour = transitionTour.Distinct(new TourStepListComparer()).ToList();
            return transitionTour;
        }


        private class TourStepListComparer : EqualityComparer<List<TransitionTourStep>>
        {
            public override bool Equals(List<TransitionTourStep> x, List<TransitionTourStep> y)
            {

                if (x.Count() != y.Count()) return false;
                for (int index = 0; index < x.Count(); index++)
                {
                    var left = x[index];
                    var right = y[index];
                    if (left.FromState != right.FromState ||
                        left.Command != right.Command ||
                        left.Event != right.Event ||
                        left.ToState != right.ToState)
                            return false;
                }
                return true;
            }

            public override int GetHashCode(List<TransitionTourStep> obj)
            {
                return obj.ToString().GetHashCode();
            }
        }

        private List<List<TransitionTourStep>> AddToTransitionTour(
                List<List<TransitionTourStep>> transitionTour,
                State fromState,
                State toState,
                List<TransitionTourStep> commandSequence)
        {

            List<TransitionTourStep> sequencesToHere = new List<TransitionTourStep>();

            // Find the transition that got us to the new State:
            var previousTransition = (from x in fromState.Transitions
                                      where x.Value.ResultingState == toState.Name
                                      select x.Value).FirstOrDefault();

            // If the transition already exists, then we end the tour,
            // otherwise we could into an infinite loop:
            if (commandSequence.Exists(p =>
                    p.FromState == fromState.Name && 
                    p.Command == previousTransition.Command &&
                    p.Event == previousTransition.Event &&
                    p.ToState == previousTransition.ResultingState))
                return transitionTour;

            commandSequence.Add(new TransitionTourStep()
                {
                    FromState = fromState.Name,
                    Command = previousTransition.Command,
                    Event = previousTransition.Event,
                    ToState = previousTransition.ResultingState
                });
            // Create a sequence that contains all transitions up to here:
            sequencesToHere.AddRange(commandSequence);

            // Iterate through each transition that comes out of the ToState
            foreach (var transition in toState.Transitions)
            {
                if (toState.Transitions.First().Value == transition.Value)
                {
                    //  We are on the first transition, so add it to the existing sequence.
                    transitionTour = AddToTransitionTour(transitionTour, toState, this[transition.Value.ResultingState], commandSequence);
                }
                else
                {
                    // We are not on the first transition, so create a new sequence.
                    List<TransitionTourStep> newCommandSequence = new List<TransitionTourStep>();
                    transitionTour.Add(newCommandSequence);
                    newCommandSequence.AddRange(sequencesToHere);
                    transitionTour = AddToTransitionTour(transitionTour, toState, this[transition.Value.ResultingState], newCommandSequence);
                }
            }
            return transitionTour;
        }

        public struct TransitionTourStep
        {
            public string FromState { get; set; }
            public string Command { get; set; }
            public string Event { get; set; }
            public string ToState { get; set; }
            public override string ToString()
            {
                return string.Format("From State: {0}, Command: {1}, Event: {2}, Resulting State: {3}",
                            FromState,
                            Command,
                            Event,
                            ToState);
            }
        }
    }
}
